package com.miao.im.support.page;

import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

/**
 * @Auther: miaoguoxin
 * @Date: 2019/10/19 14:48
 * @Description: mybatis分页拦截器
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
@Slf4j
public class PageHelper implements Interceptor {
    /**保存线程本地副本的分页对象*/
    public static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<>();

    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();


    public static <T> Page<T> startPage(int pageNumber, int pageSize) {
        return startPage(pageNumber, pageSize, true);
    }

    /**
     * @Description: 开始分页操作
     * @Param: pageNumber:当前第几页 pageSize:每页多少条 count:是否统计查询条数
     * @return:
     * @Author: miaoguoxin
     * @Date: 2019/10/19
     */
    public static <T> Page<T> startPage(int pageNumber, int pageSize, boolean count) {
        Page<T> page = new Page<>(pageNumber, pageSize, count);
        setLocalPage(page);
        return page;
    }


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Page page = getLocalPage();
        if (page == null) {
            return invocation.proceed();
        }
        try {
            RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
            // 获得拦截的对象
            StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate");
            BoundSql boundSql = delegate.getBoundSql();
            String sourceSql = boundSql.getSql();
            //不是查询语句直接跳过
            if (!this.isSelect(sourceSql)) {
                return invocation.proceed();
            }
            MetaObject boundSqlMeta = MetaObject.forObject(boundSql,
                    DEFAULT_OBJECT_FACTORY,
                    DEFAULT_OBJECT_WRAPPER_FACTORY,
                    DEFAULT_REFLECTOR_FACTORY);
            // 修改boundSql的sql
            boundSqlMeta.setValue("sql", this.getPageSql(page, sourceSql));
            //设置查询总条数
            if (page.isCount()) {
                MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");
                Connection connection = (Connection) invocation.getArgs()[0];
                page.setTotalCount(this.getTotalRecord(boundSql, mappedStatement, connection, sourceSql));
            }
        } finally {
            //一定要清理本地线程副本
            clearLocalPage();
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        // 当目标类是StatementHandler类型时，才包装目标类，否者直接返回目标本身,减少目标被代理的次数
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {
    }

    /**
     * 返回查询条数给对象page设置总记录数
     *
     *            Mapper映射语句对应的参数对象
     * @param mappedStatement
     *            Mapper映射语句
     * @param connection
     *            当前的数据库连接
     */
    private int getTotalRecord(BoundSql boundSql, MappedStatement mappedStatement, Connection connection, String sourceSql) {
        String upperSql = sourceSql.toUpperCase();
        int index = upperSql.indexOf("FROM");
        String countSql = "select count(1) " + sourceSql.substring(index);
        // 利用Configuration、查询记录数的Sql语句countSql、参数映射关系parameterMappings和参数对象page建立查询记录数对应的BoundSql对象。
        BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, countBoundSql.getParameterObject(), boundSql);
        // 通过connection建立一个countSql对应的PreparedStatement对象。
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            pstmt = connection.prepareStatement(countSql);
            parameterHandler.setParameters(pstmt);
            rs = pstmt.executeQuery();
            if (rs.next()) {
                return rs.getInt(1);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs != null) rs.close();
                if (pstmt != null) pstmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return 0;
    }

    private String getPageSql(Page page, String sql) {
        int offset = (page.getPageNumber() - 1) * page.getPageSize();
        return sql + " LIMIT " + offset + "," + page.getPageSize();
    }


    private boolean isSelect(String sql) {
        return !StrUtil.isEmpty(sql) && sql.toUpperCase().trim().startsWith("SELECT");
    }

    private static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }

    private static Page getLocalPage() {
        return LOCAL_PAGE.get();
    }

    private static void clearLocalPage() {
        LOCAL_PAGE.remove();
    }
}
